namespace Microsoft.Extensions.DependencyInjection
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Net.Http;
    using System.Reflection;
    using System.Runtime.CompilerServices;
    using Grpc.Core;
    using Grpc.Net.ClientFactory;
    using Microsoft.Extensions.DependencyModel;
    using Stee.CAP8.Communication.Grpc;
    using Stee.CAP8.Communication.GrpcInterface;

    /// <summary>
    /// Extend the interface of IServiceCollection for Grpc Client Injection
    /// Author:Hepengfei
    /// Date:2020/05/11
    /// </summary>
    public static class ContainerExtension
    {
        private const string GrpcMethodName = "AddGrpcClient";
        private const string GrpcLibraryName = "Grpc.Net.ClientFactory";
        private const StringComparison StrComparison = StringComparison.CurrentCulture;

        /// <summary>
        /// According to the GrpcClient Option, initialize the rRpc client which want to created
        /// </summary>
        /// <param name="services">Service Container</param>
        /// <param name="option">Customize gRpc Client Option</param>
        public static void AddGrpcGroupClient(this IServiceCollection services, GrpcClientOption option)
        {
            if (option != null)
            {
                if (string.IsNullOrEmpty(option.ServerAddr))
                {
                    throw new ArgumentNullException($"The target service address is null.");
                }
                if (option.GrpcClients == null || option.GrpcClients.Count == 0)
                {
                    throw new InvalidGrpcOptionException("Didn't specify which Grpc clients need be created.");
                }
                else
                {
                    var assemblyFullName = GetInvokerAssemblyFullName();
                    foreach (var tClientName in option.GrpcClients)
                    {
                        //Because the method of 'AddGrpcclient' is generic, so has to invoke this method dynamically.
                        tClientName.GrpcClientAssemblyName = assemblyFullName;
                        var assemblyName = assemblyFullName.Split(',')[0];
 
                        DynamicallyInvoke(services, tClientName, options =>
                        {
                            options.Address = new Uri(option.ServerAddr);
                            options.Interceptors.Add(new GrpcClientInterceptor(assemblyName));
                        }).ConfigureChannel(options => {
                            if (option.MaxReceiveMessageSize != null)
                            {
                                options.MaxReceiveMessageSize = option.MaxReceiveMessageSize * 1024 * 1024;
                            }
                            if (option.MaxSendMessageSize != null)
                            {
                                options.MaxSendMessageSize = option.MaxSendMessageSize * 1024 * 1024;
                            }
                        })
                        .ConfigurePrimaryHttpMessageHandler(() =>
                        {
                            return new HttpClientHandler()
                            {
                                ClientCertificateOptions = ClientCertificateOption.Automatic,
                                ServerCertificateCustomValidationCallback = (message, cert, chain, error) =>
                                {
                                    switch (option.HttpsCertCheck)
                                    {
                                        case CertCheckType.CheckCertIssuer:
                                            if (!string.IsNullOrEmpty(option.CertIssuer))
                                            {
                                                return cert.Issuer.Equals(option.CertIssuer, StringComparison.OrdinalIgnoreCase);
                                            }
                                            return false;
                                        default:
                                            return true;
                                    }
                                }
                            };
                        });
                    }
                }
            }
            else
            {
                throw new ArgumentNullException($"The {typeof(GrpcClientOption).Name} is null.");
            }
        }

        /// <summary>
        /// The program will scan the specified assembly to initialize gRPC client.
        /// </summary>
        /// <param name="services">Service Container</param>
        /// <param name="option">Customize gRpc Client Option</param>
        public static void AddGrpcClientsAutoDI(this IServiceCollection services, GrpcClientOption option)
        {
            if (option != null)
            {
                if (string.IsNullOrEmpty(option.ServerAddr))
                {
                    throw new ArgumentNullException($"The target service address is null.");
                }
                else
                {
                    var types = GetInvokerTypes();
                    foreach (var sType in types)
                    {
                        //if the type is inherited from clientbase, it's gRPC client                       
                        if (sType.IsSubclassOf(typeof(ClientBase)))
                        {
                            DynamicallyInvoke(services, sType, options =>
                            {
                                options.Address = new Uri(option.ServerAddr);
                            })
                            .ConfigureChannel(options => {
                                if (option.MaxReceiveMessageSize != null)
                                {
                                    options.MaxReceiveMessageSize = option.MaxReceiveMessageSize * 1024 * 1024;
                                }
                                if (option.MaxSendMessageSize != null)
                                {
                                    options.MaxSendMessageSize = option.MaxSendMessageSize * 1024 * 1024;
                                }
                            })
                            .ConfigurePrimaryHttpMessageHandler(() =>
                            {
                                return new HttpClientHandler()
                                {
                                    ClientCertificateOptions = ClientCertificateOption.Automatic,
                                    ServerCertificateCustomValidationCallback = (message, cert, chain, error) =>
                                    {
                                        switch (option.HttpsCertCheck)
                                        {
                                            case CertCheckType.CheckCertIssuer:
                                                if (!string.IsNullOrEmpty(option.CertIssuer))
                                                {
                                                    return cert.Issuer.Equals(option.CertIssuer, StringComparison.OrdinalIgnoreCase);
                                                }
                                                return false;
                                            default:
                                                return true;
                                        }
                                    }
                                };
                            });
                        }
                    }
                }
            }
            else
            {
                throw new ArgumentNullException($"The {typeof(GrpcClientOption).Name} is null.");
            }
        }

        /// <summary>
        /// Invoke the method of 'AddGrpcClient' dynamically
        /// </summary>
        /// <param name="services">The instance container</param>
        /// <param name="clientInfo">Which the gRpc client want to be created</param>
        /// <param name="configurations">The gRpc Client options</param>
        /// <returns></returns>
        private static IHttpClientBuilder DynamicallyInvoke(IServiceCollection services, GrpcClientInfo clientInfo, Action<GrpcClientFactoryOptions> configurations)
        {
            try
            {
                MethodInfo method = null;
                //get the generic type from the assembly of 'Grpc.Net.ClientFactory'
                var assemblies = DependencyContext.Default.CompileLibraries.Where(x => x.Name.Equals(GrpcLibraryName, StrComparison)).Select(x => Assembly.Load(x.Name));
                foreach (var assembly in assemblies)
                {
                    //look up the method for addgrpc client
                    foreach (var m in GetExtensionMethods(assembly, typeof(IServiceCollection)))
                    {

                        if (m.Name.Equals(GrpcMethodName, StrComparison))
                        {
                            method = m.DeclaringType.GetMethod(GrpcMethodName, new Type[] { typeof(IServiceCollection), typeof(string), typeof(Action<GrpcClientFactoryOptions>) });
                            if (method != null)
                            {
                                break;
                            }
                        }
                    }
                }

                if (method != null)
                {
                    //Since the method of 'AddGrpcClient' is the generic type, so have to specify the returned generic type for the method
                    var clientType = Assembly.Load(clientInfo.GrpcClientAssemblyName).DefinedTypes.FirstOrDefault(x => x.Name.Equals(clientInfo.GrpcClientTypeName, StrComparison));
                    method = method.MakeGenericMethod(clientType);
                    //dynamically invoke the method of 'AddGrpcClient' to initialzie the GrpcClient
                    var builder = method.Invoke(services, new object[] { services, clientInfo.GrpcClientTypeName, configurations });
                    if (builder is IHttpClientBuilder)
                    {
                        return builder as IHttpClientBuilder;
                    }   
                }
                return null;
            }
            catch (Exception ex)
            {
                throw new Cap8GrpcException(ex.Message);
            }
        }

        /// <summary>
        /// Invoke the method of 'AddGrpcClientsAutoDI' dynamically
        /// </summary>
        /// <param name="services">The instance container</param>
        /// <param name="gRpcType">Which the gRpc client type want to be created</param>
        /// <param name="configurations">The gRpc Client options</param>
        /// <returns></returns>
        private static IHttpClientBuilder DynamicallyInvoke(IServiceCollection services, Type gRpcType, Action<GrpcClientFactoryOptions> configurations)
        {
            try
            {
                MethodInfo method = null;
                //get the generic type from the assembly of 'Grpc.Net.ClientFactory'
                var assemblies = DependencyContext.Default.CompileLibraries.Where(x => x.Name.Equals(GrpcLibraryName, StrComparison)).Select(x => Assembly.Load(x.Name));
                foreach (var assembly in assemblies)
                {
                    //look up the method for addgrpc client
                    foreach (var m in GetExtensionMethods(assembly, typeof(IServiceCollection)))
                    {
                        if (m.Name.Equals(GrpcMethodName, StrComparison))
                        {
                            method = m.DeclaringType.GetMethod(GrpcMethodName, new Type[] { typeof(IServiceCollection), typeof(string), typeof(Action<GrpcClientFactoryOptions>) });
                            if (method != null)
                            {
                                break;
                            }
                        }
                    }
                }

                if (method != null)
                {
                    //Since the method of 'AddGrpcClient' is the generic type, so have to specify the returned generic type for the method
                    method = method.MakeGenericMethod(gRpcType);
                    //dynamically invoke the method of 'AddGrpcClient' to initialzie the GrpcClient
                    var builder = method.Invoke(services, new object[] { services, gRpcType.Name, configurations });
                    if (builder is IHttpClientBuilder)
                    {
                        return builder as IHttpClientBuilder;
                    }    
                }
                return null;
            }
            catch (Exception ex)
            {
                throw new Cap8GrpcException(ex.Message);
            }
        }

        /// <summary>
        /// Get the list of method info from the <see cref="Assembly"/>.
        /// </summary>
        /// <param name="assembly"><see cref="Assembly"/></param>
        /// <param name="extendedType"><see cref="Type"/></param>
        /// <returns></returns>
        private static IEnumerable<MethodInfo> GetExtensionMethods(Assembly assembly, Type extendedType)
        {
            var query = from type in assembly.GetTypes()
                        where type.IsSealed && !type.IsGenericType && !type.IsNested
                        from method in type.GetMethods(BindingFlags.Static
                            | BindingFlags.Public | BindingFlags.NonPublic)
                        where method.IsDefined(typeof(ExtensionAttribute), false)
                        where method.GetParameters()[0].ParameterType == extendedType
                        select method;
            return query;
        }

        /// <summary>
        /// Get types of invoker
        /// </summary>
        /// <returns></returns>
        private static Type[] GetInvokerTypes()
        {
            var trace = new StackTrace();
            return trace.GetFrame(2).GetMethod().Module.GetTypes();
        }

        /// <summary>
        /// Get Assembly Full Name of invoker
        /// </summary>
        /// <returns></returns>
        private static string GetInvokerAssemblyFullName()
        {
            var trace = new StackTrace();
            return trace.GetFrame(2).GetMethod().Module.Assembly.FullName;
        }
    }
}
